package com.haowu.auth.service;

import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.haowu.auth.form.RegisterBody;
import com.haowu.auth.form.WechatLoginBody;
import com.haowu.auth.properties.UserPasswordProperties;
import com.haowu.common.core.constant.CacheConstants;
import com.haowu.common.core.constant.Constants;
import com.haowu.common.core.domain.LoginParams;
import com.haowu.common.core.enums.DeviceType;
import com.haowu.common.core.enums.LoginType;
import com.haowu.common.core.enums.UserType;
import com.haowu.common.core.exception.user.CaptchaExpireException;
import com.haowu.common.core.exception.user.UserException;
import com.haowu.common.core.utils.MessageUtils;
import com.haowu.common.core.utils.ServletUtils;
import com.haowu.common.core.utils.SpringUtils;
import com.haowu.common.core.utils.StringUtils;
import com.haowu.common.core.utils.ip.AddressUtils;
import com.haowu.common.log.event.LogininforEvent;
import com.haowu.common.redis.utils.RedisUtils;
import com.haowu.common.satoken.utils.LoginHelper;
import com.haowu.customer.api.RemoteCustomerService;
import com.haowu.customer.api.domain.HwAppCustomer;
import com.haowu.customer.api.model.LoginCustomer;
import com.haowu.customer.api.model.XcxLoginCustomer;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.Objects;
import java.util.function.Supplier;

/**
 * @author lky
 *
 * 顾客登录校验,可选登录方式：手机号密码登录、短信验证码登录、小程序登录、第三方授权登录
 * 手机号密码登录：需要注册,账号即为手机号,最简单的登录方式
 * 短信验证码登录：通过短信验证码登录,手机号未查询到则创建新用户,无登录密码
 * 小程序登录：通过小程序授权获取到的code登录,未查询到则创建新用户,无登录密码
 * 第三方授权登录：以微信授权登录为例,通过授权获取到的openid登录,未查询到则创建新用户,无登录密码
 * <p>
 * 可能存在问题1:微信授权登录与小程序登录属于同一个微信主体,两者需要通过unionId同步数据
 *            此时需要开发者去微信开放平台绑定小程序,如果没有绑定则wx.login不会返回unionid
 * 此时默认开发者通过微信开放平台绑定小程序,即获取openid的同时也可以获取到unionid
 * 详细文档参考:<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html">...</a>
 * <p>
 * 可能存在问题2:微信小程序登录后可以通过拉起授权获取手机号来绑定手机,此时若该手机已在app中注册过,两者是否需要同步数据?
 *            如果不同步,那么系统中就会存在两个相同手机号的用户,如果同步,那么两方都存在业务数据的情况下该以哪一方为主体?
 */
@Service
public class CusLoginService {
    @DubboReference
    private RemoteCustomerService customerService;
    @Autowired
    private UserPasswordProperties userPasswordProperties;


    /**
     * 账号密码登录
     * 可以直接注册时就设置密码，也可以先短信验证码登录，之后再设置登录密码
     * 只有存在登录密码的账号才能使用账密登录
     * @param account 账号,即手机号
     * @param password 登录密码
     * @return 登陆成功后返回token
     */
    public String login(String account, String password) {
        LoginCustomer userInfo = customerService.getCustomerInfo(account);
        checkLogin(LoginType.PASSWORD, account, () -> !BCrypt.checkpw(password, userInfo.getPassword()));
        // 获取登录token
        LoginHelper.loginByDevice(userInfo, DeviceType.APP);
        recordLogininfor(account, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
        return StpUtil.getTokenValue();
    }

    /**
     * 短信验证码登录
     * 初次登录会新建用户,并将账号名设置为手机号,但不会有登录密码
     * 之后可以单独设置登录密码,设置后就可以使用账密登录
     * @param phonenumber 手机号
     * @param smsCode 短信验证码
     * @return 登陆成功后返回token
     */
    public String smsLogin(String phonenumber, String smsCode) {
        //无论库中是否已有手机号都应先校验验证码
        checkLogin(LoginType.SMS, phonenumber, () -> !validateSmsCode(phonenumber, smsCode));
        //短信验证码登录应能为首次登录的用户创建新账号
        LoginCustomer userInfo = customerService.getCustomerInfoByPhonenumber(phonenumber);
        if(ObjectUtil.isNotNull(userInfo)){
            //登录并生成token
            LoginHelper.loginByDevice(userInfo, DeviceType.APP);
            recordLogininfor(userInfo.getUsername(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
            return StpUtil.getTokenValue();
        }else{
            //未查询到该手机号,则需要创建新账号并直接登录,此时并没有设置密码
            HwAppCustomer appCustomer = new HwAppCustomer();
            appCustomer.setUserName(phonenumber);
            appCustomer.setUserPhone(phonenumber);
            Boolean result = customerService.registerCustomer(appCustomer);
            if(result){
                LoginCustomer loginCustomer = customerService.getCustomerInfoByPhonenumber(phonenumber);
                //登录并生成token
                LoginHelper.loginByDevice(loginCustomer, DeviceType.APP);
                recordLogininfor(phonenumber, Constants.REGISTER, MessageUtils.message("user.register.success"));
                recordLogininfor(userInfo.getUsername(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
                return StpUtil.getTokenValue();
            }else{
                recordLogininfor(phonenumber, Constants.REGISTER, MessageUtils.message("user.register.error"));
                throw new UserException("user.register.error");
            }
        }
    }

    /**
     * 微信小程序登录
     * @param wechatLoginBody 小程序code
     * @return 登陆成功后返回token
     */
    public String xcxLogin(WechatLoginBody wechatLoginBody) {
        // xcxCode 为 小程序调用 wx.login 授权后获取
        // TODO 实现不同平台同一用户的数据同步
        String openid = wechatLoginBody.getOpenid();
        String unionid = wechatLoginBody.getUnionid();
        String phonenumber = wechatLoginBody.getPhonenumber();
        XcxLoginCustomer userInfo = customerService.getCustomerInfoByOpenid(openid,unionid,phonenumber);
        // 生成token
        LoginHelper.loginByDevice(userInfo, DeviceType.XCX);
        recordLogininfor(userInfo.getUsername(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
        return StpUtil.getTokenValue();
    }

    /**
     * 退出登录
     */
    public void logout() {
        try {
            LoginParams loginUser = LoginHelper.getLoginUser();
            StpUtil.logout();
            recordLogininfor(loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
        } catch (NotLoginException ignored) {
        }
    }

    /**
     * 新顾客账号密码注册
     * 账号即为手机号,如果该手机号已存在,应注册失败并提示
     * 不能直接绑定以前仅使用短信验证码登录的用户,因为不能保证这两者是同一人
     * 举例:如果你知道同事的手机号并且同事以前只使用验证码登录，那么你用他的手机号注册并成功获取到验证码,
     *      注册成功后就可以完全继承同事的账号信息,且使用账密登录可以完全跳过同事的的感知,这是顾客不可接受的
     *
     * @param registerBody 注册传参
     */
    public void register(RegisterBody registerBody) {
        String username = registerBody.getUsername();
        String password = registerBody.getPassword();
        // 校验用户类型是否存在
        String userType = UserType.getUserType(registerBody.getUserType()).getUserType();
        // 注册用户信息
        HwAppCustomer account = new HwAppCustomer();
        account.setUserName(username);
        account.setPassword(BCrypt.hashpw(password));
        boolean regFlag = customerService.registerCustomer(account);
        if (!regFlag) {
            throw new UserException("user.register.error");
        }
        recordLogininfor(username, Constants.REGISTER, MessageUtils.message("user.register.success"));
    }

    /**
     * 记录登录信息
     *
     * @param username 用户名
     * @param status   状态
     * @param message  消息内容
     */
    public void recordLogininfor(String username, String status, String message) {
        HttpServletRequest request = ServletUtils.getRequest();
        final UserAgent userAgent = UserAgentUtil.parse(Objects.requireNonNull(request).getHeader("User-Agent"));
        final String ip = ServletUtils.getClientIP(request);
        String address = AddressUtils.getRealAddressByIP(ip);
        // 获取客户端操作系统
        String os = userAgent.getOs().getName();
        // 获取客户端浏览器
        String browser = userAgent.getBrowser().getName();
        // 封装对象
        LogininforEvent logininfor = new LogininforEvent();
        logininfor.setUserName(username);
        logininfor.setIpaddr(ip);
        logininfor.setLoginLocation(address);
        logininfor.setBrowser(browser);
        logininfor.setOs(os);
        logininfor.setMsg(message);
        // 日志状态
        if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) {
            logininfor.setStatus(Constants.LOGIN_SUCCESS_STATUS);
        } else if (Constants.LOGIN_FAIL.equals(status)) {
            logininfor.setStatus(Constants.LOGIN_FAIL_STATUS);
        }
        SpringUtils.context().publishEvent(logininfor);
    }

    /**
     * 校验短信验证码
     */
    private boolean validateSmsCode(String phonenumber, String smsCode) {
        String code = RedisUtils.getCacheObject(CacheConstants.CAPTCHA_CODE_KEY + phonenumber);
        if (StringUtils.isBlank(code)) {
            recordLogininfor(phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
            throw new CaptchaExpireException();
        }
        return code.equals(smsCode);
    }

    /**
     * 登录校验(短信验证码、密码登录等)
     */
    private void checkLogin(LoginType loginType, String username, Supplier<Boolean> supplier) {
        String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
        String loginFail = Constants.LOGIN_FAIL;
        Integer maxRetryCount = userPasswordProperties.getMaxRetryCount();
        Integer lockTime = userPasswordProperties.getLockTime();
        // 获取用户登录错误次数(可自定义限制策略 例如: key + username + ip)
        Integer errorNumber = RedisUtils.getCacheObject(errorKey);
        // 锁定时间内登录 则踢出
        if (ObjectUtil.isNotNull(errorNumber) && errorNumber.equals(maxRetryCount)) {
            recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
            throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
        }
        if (supplier.get()) {
            // 是否第一次输入
            errorNumber = ObjectUtil.isNull(errorNumber) ? 1 : errorNumber + 1;
            // 达到规定错误次数 则锁定登录
            if (errorNumber.equals(maxRetryCount)) {
                RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
                recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
                throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
            } else {
                // 未达到规定错误次数 则递增
                RedisUtils.setCacheObject(errorKey, errorNumber);
                recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
                throw new UserException(loginType.getRetryLimitCount(), errorNumber);
            }
        }
        // 登录成功 清空错误次数
        RedisUtils.deleteObject(errorKey);
    }
}
